/*
 * Copyright (C) 2012 Tapas Mobile Ltd.  All Rights Reserved.
 */

package com.tools.cleanmaster.appinfo;

import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.os.Build;

import com.tools.cleanmaster.BuildConfig;
import com.tools.cleanmaster.CleanApplication;
import com.tools.cleanmaster.utils.LanguageHelper;
import com.tools.cleanmaster.utils.LogHelper;

import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

/**
 * for manage all the apps info
 *
 * @author billy
 * @see AppInfoCompat
 */
@TargetApi(Build.VERSION_CODES.FROYO)
public final class AppManagerCompat {
    private static final class AppCache {
        public String label;
        public WeakReference<Drawable> icon;
        public boolean isValid;
    }

    public interface AppChangeListener {
        void onAppChanged(int type, String pkgName);
    }

    public static final int CHANGE_TYPE_ADD = 1;
    public static final int CHANGE_TYPE_REMOVE = 2;
    public static final int CHANGE_TYPE_UPDATE = 3;
    public static final int CHANGE_TYPE_SYSUPDATE = 4;
    public static final int CHANGE_TYPE_NONE = 0;

    private static final boolean DEBUG = BuildConfig.DEBUG_LOG;
    private static final String TAG = "AppManager";

    private static AppManagerCompat sInstance;
    private static Drawable sDefaultIcon;

    private Context mContext;
    private PackageManager mPm;
    private final HashMap<String, AppInfoCompat> mAppMaps;
    private final HashMap<String, AppInfoCompat> mInstallAppMaps;
    /**
     * Icon caches map
     */
    private HashMap<String, AppCache> mIconMaps;
    private String mLocale;
    private final Object mLock = new Object();
    private ArrayList<AppChangeListener> mReceivers = new ArrayList<AppChangeListener>();

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (DEBUG) {
                LogHelper.d(TAG, "receive " + action);
            }
            if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action) ||
                    Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
                onApplicationsChanged(context, intent);
            } else if (Intent.ACTION_MEDIA_BAD_REMOVAL.equals(action)) {
                rescanInstall();
            } else {
                String pkgName = URI.create(intent.getDataString()).getSchemeSpecificPart();
                boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
                if (Intent.ACTION_PACKAGE_ADDED.equals(action) && !replacing) {
                    onPackageAdded(pkgName);
                } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action) && !replacing) {
                    onPackageRemoved(pkgName);
                } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action) ||
                        Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
                    onPackageUpdate(pkgName);
                }
            }
        }
    };

    private AppManagerCompat() {
        mContext = CleanApplication.getInstance();
        mPm = mContext.getPackageManager();
        mAppMaps = new HashMap<String, AppInfoCompat>();
        mInstallAppMaps = new HashMap<String, AppInfoCompat>();
        mIconMaps = new HashMap<String, AppCache>();

        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
        filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
        // TODO: we should add bad_removal support??? maybe for few devices
        //filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL);
        mContext.registerReceiver(mReceiver, filter);

        IntentFilter filter2 = new IntentFilter();
        filter2.addAction(Intent.ACTION_PACKAGE_ADDED);
        filter2.addAction(Intent.ACTION_PACKAGE_REPLACED);
        filter2.addAction(Intent.ACTION_PACKAGE_REMOVED);
        filter2.addAction(Intent.ACTION_PACKAGE_CHANGED);
        filter2.addDataScheme("package");
        mContext.registerReceiver(mReceiver, filter2);
    }

    /**
     * get the AppManager instance
     *
     * @return
     */
    public synchronized static AppManagerCompat getInstance() {
        if (sInstance == null) {
            sInstance = new AppManagerCompat();
        }
        return sInstance;
    }

    private synchronized void initApps() {
        if (mAppMaps.size() == 0) {
            //   String lang = GlobalSettingActivity.getLocaleSettings(mContext).toString();
            mLocale = LanguageHelper.getLocale(mContext).toString();
            rescanAll();
        }
    }

    public void registerListener(AppChangeListener listener) {
        synchronized (mReceivers) {
            mReceivers.add(listener);
        }
    }

    public void unregisterListener(AppChangeListener listener) {
        synchronized (mReceivers) {
            mReceivers.remove(listener);
        }
    }

    public void notifyChanged(int type, String pkgName) {
        if (DEBUG) {
            LogHelper.d(TAG, "notifyChanged " + type + " " + pkgName);
        }
        final ArrayList<AppChangeListener> receivers;
        synchronized (mReceivers) {
            receivers = new ArrayList<AppChangeListener>(mReceivers);
        }
        for (AppChangeListener listener : receivers) {
            if (DEBUG) {
                LogHelper.d(TAG, "notify " + listener.getClass().getName());
            }
            listener.onAppChanged(type, pkgName);
        }
    }

    PackageInfo fetchPackage(String pkgName) {
        try {
            return mPm.getPackageInfo(pkgName, PackageManager.GET_DISABLED_COMPONENTS |
                    PackageManager.GET_SIGNATURES);
        } catch (NameNotFoundException e) {
            try {
                return mPm.getPackageInfo(pkgName, PackageManager.GET_DISABLED_COMPONENTS |
                        PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_SIGNATURES);
            } catch (NameNotFoundException ee) {
                if (DEBUG) {
                    ee.printStackTrace();
                }
            }
        }
        return null;
    }

    private void rescanAll() {
        List<PackageInfo> allApps = getInstalledPackages(mPm,
                PackageManager.GET_DISABLED_COMPONENTS | PackageManager.GET_UNINSTALLED_PACKAGES);
        HashMap<String, AppInfoCompat> mapsAll = new HashMap<String, AppInfoCompat>();
        HashMap<String, AppInfoCompat> mapsInstall = new HashMap<String, AppInfoCompat>();
        boolean higherApiLevel17 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
        for (PackageInfo app : allApps) {
            AppInfoCompat info = new AppInfoCompat(app);
            mapsAll.put(info.pkgName, info);
            if (higherApiLevel17) {
                if ((app.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) == 0) {
                    mapsInstall.put(info.pkgName, info);
                }
            }
        }
        if (!higherApiLevel17) {
            List<PackageInfo> installApps = getInstalledPackages(mPm, PackageManager.GET_DISABLED_COMPONENTS);
            for (PackageInfo app : installApps) {
                AppInfoCompat appInfo = mapsAll.get(app.packageName);
                if (appInfo != null) {
                    mapsInstall.put(app.packageName, appInfo);
                }
            }
        }
        synchronized (mLock) {
            mAppMaps.clear();
            mAppMaps.putAll(mapsAll);

            mInstallAppMaps.clear();
            mInstallAppMaps.putAll(mapsInstall);
        }
    }

    private void rescanInstall() {
        List<PackageInfo> list = mPm.getInstalledPackages(PackageManager.GET_DISABLED_COMPONENTS);
        synchronized (mLock) {
            mInstallAppMaps.clear();
            for (PackageInfo app : list) {
                AppInfoCompat appInfo = mAppMaps.get(app.packageName);
                if (null != appInfo) {
                    mInstallAppMaps.put(app.packageName, appInfo);
                }
            }
        }
        notifyChanged(CHANGE_TYPE_NONE, null);
    }

    /**
     * get the applications list exclude uninstalled apps, including disabled
     *
     * @return
     */
    public static ArrayList<AppInfoCompat> getInstalledApplications() {
        return getInstance().getApplicationList(true);
    }

    /**
     * get the applications list including uninstalled apps, including disabled
     *
     * @return
     */
    public static ArrayList<AppInfoCompat> getAllApplications() {
        return getInstance().getAllApplicationList(true);
    }

    /**
     * 返回所有用户应用（不包含系统应用）
     *
     * @return
     */
    public static ArrayList<AppInfoCompat> getAllUserApplications() {
        ArrayList<AppInfoCompat> allApps = getInstance().getAllApplicationList(true);
        ArrayList<AppInfoCompat> allUserApps = new ArrayList<AppInfoCompat>();
        for (AppInfoCompat appInfo : allApps) {
            if (!appInfo.isSystem) {
                allUserApps.add(appInfo);
            }
        }
        return allUserApps;
    }

    /**
     * get the applications list exclude uninstalled apps
     *
     * @param disabled whether including disabled apps
     * @return
     * @see AppManagerCompat#getInstalledApplications()
     */
    public ArrayList<AppInfoCompat> getApplicationList(boolean disabled) {
        initApps();
        ArrayList<AppInfoCompat> ret;
        synchronized (mLock) {
            ret = new ArrayList<AppInfoCompat>(mInstallAppMaps.values());
        }

        if (disabled) {
            return ret;
        } else {
            Iterator<AppInfoCompat> it = ret.iterator();
            while (it.hasNext()) {
                AppInfoCompat info = it.next();
                if (!info.enable()) {
                    it.remove();
                }
            }

            return ret;
        }
    }

    /**
     * get the applications list including uninstalled apps
     *
     * @param disabled whether including disabled apps
     * @return
     * @see AppManagerCompat#getAllApplications()
     */
    public ArrayList<AppInfoCompat> getAllApplicationList(boolean disabled) {
        initApps();
        ArrayList<AppInfoCompat> ret;
        synchronized (mLock) {
            ret = new ArrayList<AppInfoCompat>(mAppMaps.values());
        }

        if (disabled) {
            return ret;
        } else {
            Iterator<AppInfoCompat> it = ret.iterator();
            while (it.hasNext()) {
                AppInfoCompat info = it.next();
                if (!info.enable()) {
                    it.remove();
                }
            }

            return ret;
        }
    }

    /**
     * it's no cached, just same as {@link PackageManager#getApplicationEnabledSetting(String)}
     *
     * @param pkgName
     * @return
     */
    public static int getApplicationEnabledSetting(String pkgName) {
        try {
            return CleanApplication.getInstance().getPackageManager().getApplicationEnabledSetting(pkgName);
        } catch (Exception e) {
            // should not be here, but .....
        }
        return PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
    }

    public boolean isAppEnabled(String pkgName) {
        AppInfoCompat info = getApplication(pkgName);
        if (info != null) {
            return info.enable();
        }
        return false;
    }

    public AppInfoCompat getApplication(String pkgName) {
        if (pkgName == null) {
            return null;
        }
        initApps();
        synchronized (mLock) {
            return mAppMaps.get(pkgName);
        }
    }

    public AppInfoCompat getInstalledApplication(String pkgName) {
        if (pkgName == null) {
            return null;
        }
        initApps();
        synchronized (mLock) {
            return mInstallAppMaps.get(pkgName);
        }
    }

    /**
     * get the AppInfo2 object according to package name
     *
     * @param pkgName
     * @return never be null
     * @throws NameNotFoundException
     */
    public static AppInfoCompat getAppInfo(String pkgName) throws NameNotFoundException {
        AppInfoCompat info = getInstance().getApplication(pkgName);
        if (info == null) {
            throw new NameNotFoundException(pkgName + " not found!");
        }
        return info;
    }

    private AppCache newAppCache(String pkgName) {
        AppCache cache = new AppCache();
        mIconMaps.put(pkgName, cache);
        return cache;
    }

    private AppCache getAppCache(String pkgName) {
        if (pkgName == null) {
            return null;
        }
        return mIconMaps.get(pkgName);
    }

    private static AppCache setAppCache(AppCache cache, String pkgName, String label, Drawable icon) {
        if (cache == null) {
            cache = getInstance().newAppCache(pkgName);
        }
        if (label != null) {
            cache.label = label;
        }
        if (icon != null) {
            cache.icon = new WeakReference<Drawable>(icon);
            cache.isValid = true;
        }
        return cache;
    }

    /**
     * fetch the cached app label
     * !请注意，此方法是给{@link AppInfoCompat}的getLabel方法使用
     * 因此，外部不要用此方法来获取Lable，而应该使用AppInfoCompat的getLabel方法
     *
     * @param app the AppInfo2 fetched by getAppInfoNoFail
     * @return the label or package name
     */
    static String getLabel(AppInfoCompat app) {
        String pkgName = app.pkgName;
        AppCache cache = getInstance().getAppCache(pkgName);
        String label = null;
        if (cache != null) {
            label = cache.label;
        }
        return label == null ? pkgName : label;
    }

    public static void setLabel(String pkgName, String label) {
        AppCache cache = getInstance().getAppCache(pkgName);
        setAppCache(cache, pkgName, label, null);
    }

    public static Drawable getIcon(AppInfoCompat app) {
        String pkgName = app.pkgName;
        AppCache cache = getInstance().getAppCache(pkgName);
        Drawable icon = null;
        if (cache == null || cache.icon == null || (icon = cache.icon.get()) == null ||
                (!cache.isValid && app.isValid())) {
            PackageInfo info = app.getPackage();
            if (info != null) {
                try {
                    icon = info.applicationInfo.loadIcon(
                            CleanApplication.getInstance().getPackageManager());
                } catch (OutOfMemoryError e) {
                    LogHelper.e(TAG, e.toString());
                } catch (Throwable e) {
                    LogHelper.e(TAG, e.toString());
                }
                if (icon != null) {
                    setAppCache(cache, pkgName, null, icon);
                }
            }
        }
        if (icon == null) {
            synchronized (AppManagerCompat.class) {
                if (sDefaultIcon == null) {
                    sDefaultIcon = CleanApplication.getInstance().getResources()
                            .getDrawable(android.R.drawable.sym_def_app_icon);
                }
                icon = sDefaultIcon;
            }
        }
        return icon;
    }

    /**
     * get the installed AppInfo2 object according to package name
     *
     * @param pkgName
     * @return never be null
     * @throws NameNotFoundException
     */
    public static AppInfoCompat getInstalledAppInfo(String pkgName) throws NameNotFoundException {
        AppInfoCompat info = getInstance().getInstalledApplication(pkgName);
        if (info == null) {
            throw new NameNotFoundException(pkgName + " not found!");
        }
        return info;
    }

    /**
     * get the AppInfo2 object according to package name
     *
     * @param pkgName
     * @return
     */
    public static AppInfoCompat getAppInfoNoFail(String pkgName) {
        return getAppInfoNoFail(pkgName, false);
    }

    public static AppInfoCompat getAppInfoNoFail(String pkgName, boolean installed) {
        AppInfoCompat info;
        if (installed) {
            info = getInstance().getInstalledApplication(pkgName);
        } else {
            info = getInstance().getApplication(pkgName);
        }
        if (info == null) {
            info = new AppInfoCompat(pkgName);
        }
        return info;
    }

    /**
     * when locale change, make AppInfo2.mLable invalid
     */
    public void onLocaleChange() {
        String lang = LanguageHelper.getLocale(mContext).toString();
        //String lang = GlobalSettingActivity.getLocaleSettings(mContext).toString();
        if (DEBUG) {
            LogHelper.d(TAG, "onLocaleChange " + lang);
        }
        if (lang.equals(mLocale)) {
            return;
        }
        mLocale = lang;
        synchronized (mLock) {
            for (AppInfoCompat info : mAppMaps.values()) {
                info.onLocaleChange();
            }
        }
        notifyChanged(CHANGE_TYPE_NONE, null);
    }

    public void onPackageAdded(String pkgName) {
        if (DEBUG) {
            LogHelper.d(TAG, "onPackageAdd " + pkgName);
        }
        initApps();
        PackageInfo app = fetchPackage(pkgName);
        if (app == null) {
            return;
        }
        synchronized (mLock) {
            AppInfoCompat info = new AppInfoCompat(app);
            mAppMaps.put(info.pkgName, info);
            if (info.getInstallTime() > 0) {
                mInstallAppMaps.put(info.pkgName, info);
            }
        }
        notifyChanged(CHANGE_TYPE_ADD, pkgName);
    }

    public void onPackageRemoved(String pkgName) {
        if (DEBUG) {
            LogHelper.d(TAG, "onPackageRemove " + pkgName);
        }
        initApps();
        synchronized (mLock) {
            mAppMaps.remove(pkgName);
            mInstallAppMaps.remove(pkgName);
            markInvalid(pkgName);
        }
        notifyChanged(CHANGE_TYPE_REMOVE, pkgName);
    }

    private void markInvalid(String pkgName) {
        if (pkgName == null) {
            return;
        }
        AppCache cache = mIconMaps.get(pkgName);
        if (cache != null) {
            cache.isValid = false;
        }
    }

    public void onPackageUpdate(String pkgName) {
        if (DEBUG) {
            LogHelper.d(TAG, "onPackageUpdate " + pkgName);
        }
        initApps();
        boolean isSystem = false;
        synchronized (mLock) {
            mAppMaps.remove(pkgName);
            mInstallAppMaps.remove(pkgName);
            markInvalid(pkgName);
            PackageInfo app = fetchPackage(pkgName);
            if (app == null) {
                return;
            }
            AppInfoCompat info = new AppInfoCompat(app);
            isSystem = info.isSystem;
            mAppMaps.put(info.pkgName, info);
            if (info.getInstallTime() > 0) {
                mInstallAppMaps.put(info.pkgName, info);
            }
        }
        notifyChanged(CHANGE_TYPE_UPDATE, pkgName);
        if (isSystem) {
            notifyChanged(CHANGE_TYPE_SYSUPDATE, pkgName);
        }
    }

    private void onApplicationsChanged(Context cxt, Intent intent) {
        String[] rpkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
        if (rpkgList == null || rpkgList.length == 0) {
            return;
        }
        initApps();
        boolean available = Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(intent.getAction());
        synchronized (mLock) {
            for (String pkg : rpkgList) {
                AppInfoCompat info = getApplication(pkg);
                if (info == null) {
                    continue;
                }
                info.onPackageUpdate();
                if (available) {
                    mInstallAppMaps.put(pkg, info);
                } else {
                    mInstallAppMaps.remove(pkg);
                }
            }
        }
        // 如果rpkgList长度为1，则应该将此pkgName传入的，否则后面会丢失pkgName信息
        notifyChanged(CHANGE_TYPE_NONE, rpkgList.length == 1 ? rpkgList[0] : null);
    }

    /**
     * Wrapper {@link PackageManager#getInstalledPackages(int)} and eat the {@link android.os
     * .TransactionTooLargeException}
     *
     * @param pm
     * @param flags
     * @return
     */
    private static List<PackageInfo> getInstalledPackages(PackageManager pm, int flags) {
        List<PackageInfo> installPackages;
        try {
            installPackages = pm.getInstalledPackages(flags);
        } catch (Throwable transactionTooLargeException) {
            // android.os.TransactionTooLargeException
            installPackages = new ArrayList<PackageInfo>();
        }
        return installPackages;
    }
}
